昨天,我們把一支 Web API 裡會出現的 Method、Path、Parameter、Request、Response、Status Code,一個一個放回同一張 HTTP 溝通地圖裡。
但知道 API 是怎麼說話之後,下一個問題是:
Frontend 和 Backend 怎麼確定,彼此理解的是同一套規則?
假設 Frontend 以為 Backend 會回:
{
"title": "週末下午茶"
}
但 Backend 實際回的是:
{
"name": "週末下午茶"
}
問題不一定是哪一邊 Code 寫錯了。
而是:
Frontend 和 Backend 對這支 API 的規則,根本沒有對上。
那到底要怎麼把這些規則先講清楚,讓兩邊都照同一套來?
這就是今天的主角:
API Contract(API 契約)。
API Contract(API 契約),可以先理解成:
API 的使用者和提供者,先把「怎麼呼叫、怎麼回應」講清楚的規則。
放在前後端分離的 Web 專案裡,就是 Frontend 和 Backend 要先對齊:
要呼叫哪一支 API?
要傳什麼資料?
資料長什麼樣?
成功會收到什麼?
使用這支 API 有哪些限制?
所以 API Contract 不是某一個固定格式的檔案。
真正重要的是,把雙方要遵守的內容講清楚。
先直接看一份人類比較好讀的示意版本。
假設現在要定義:
取得單一活動
API:取得單一活動
Method(HTTP 方法)
GET
Path(路徑)
/api/activities/{id}
Path Parameter(路徑參數)
id
→ string 字串
→ required 必填
→ Activity ID
Authentication(身分驗證)
需要登入
Success Response(成功回應)
200 OK
Response Body(回應內容)
{
"id": "abc123",
"title": "週末下午茶",
"location": "台北"
}
Response Schema(回應資料結構)
id
→ string 字串
→ required:回應中必須存在
title
→ string 字串
→ required:回應中必須存在
location
→ string 字串
→ nullable 可以是 null
Failure Response(失敗回應)
404 Not Found
→ 找不到指定活動
一支 API 的契約,不只是:
「這個網址可以打。」
它還要把:
怎麼呼叫
↓
要帶什麼
↓
資料怎麼規定
↓
最後會拿到什麼
講清楚。
Method、Path、Parameter 這些昨天已經拆過的部分,今天就先跳過啦~
接下來,就來看這份 Contract 裡,還沒被我們拆解過的 Schema(資料結構規格)。
假設一支建立活動的 API 只說:
請傳一個 Request Body。
這句話其實還遠遠不夠。
因為 Frontend 下一秒就會開始問:
裡面有哪些欄位?
title 是文字還是數字?
一定要傳嗎?
location 沒資料時怎麼辦?
時間要用什麼格式?
這些就是 Schema(資料結構規格)在處理的事情。
例如:
Request Body
│
├─ title
│ ├─ type:string 字串
│ └─ required:必填
│
├─ location
│ ├─ type:string 字串
│ └─ optional:選填
│
└─ startAt
├─ type:string 字串
├─ format:date-time 日期時間
└─ required:必填
也就是說,Contract 不只要說:
「有一個 title。」
還要說:
「title 是 string,而且必填。」
甚至「欄位可以不出現」,和「欄位存在但值是 null」,對使用端來說也可能代表兩種不同的 Contract。
但 Contract 也不是把這些欄位填完就結束了。
接下來還要決定,Frontend 之後到底要照哪一套規則來送資料、讀資料。
例如:
日期時間統一用什麼格式?
沒有資料時,
是回 null,
還是乾脆不出現這個欄位?
列表資料很多時,
要不要 Pagination(分頁)?
像分頁就可能設計成:
?page=2
也可能是:
?cursor=abc123
分頁本身還有不同做法,今天先不往下展開。
重點是:
只要這個決定會改變 Frontend 怎麼呼叫 API、怎麼讀資料,它就可能成為 API Contract 的一部分。
假設 Backend 原本回:
{
"title": "週末下午茶"
}
Frontend 已經按照這份 Contract 寫:
讀取 response.title
後來 Backend 把欄位改成:
{
"name": "週末下午茶"
}
Backend 自己可能還是正常運作。
但既有 Frontend 已經按照原本的 Contract 寫好了。
現在就可能壞掉。
這類會破壞既有使用端相容性的修改,通常就屬於:
Breaking Change(破壞性變更)。
所以一支 API 一旦開始被別人依賴,問題就不再只是:
「Backend 現在能不能跑?」
還要問:
「已經依賴這份 API 的人,會不會被我一起改壞?」
既然這些規則真的會被別人依賴,那就不能只存在 Backend Code、聊天室紀錄,或某個人的腦袋裡。
這時就會進到下一層:
API Documentation(API 文件)。
API Contract 是:
大家共同遵守的規則本身。
API Documentation(API 文件)則是:
把這些規則整理、保存成可以查閱的資訊。
所以兩邊內容看起來本來就會很像。
API 文件裡一樣可能會看到:
Method
Path
Parameters
Request Body
Schema
Responses
Authentication
因為它本來就是在描述那份 Contract。
差別在於:
API Contract
→ 我們承諾什麼
API Documentation
→ 把這些承諾整理成可以查閱的內容
而 API 文件也不一定要使用某一個特定工具。
最簡單可以自己寫 Markdown:
## 取得活動詳情
GET /api/activities/{id}
需要登入。
Path Parameter:
- id:Activity ID
成功:
- 200
找不到:
- 404
也可以整理在 README、Wiki、Notion,或其他 API 文件工具裡。
真正重要的是:
那份文件是不是還跟現在真正的 API 一致。
第一次拿到一份 API 文件,裡面可能密密麻麻列了一大堆 Endpoint。
最簡單的讀法,是沿著一次 Request → Response 的方向看:
Method + Path
→ 這支 API 做什麼?
Parameters / Request Body
→ 我要傳什麼?
Schema
→ 每個欄位怎麼規定?
Responses
→ 不同結果會收到什麼?
Authentication
→ 這支 API 需不需要身分驗證?
其實就是把昨天學過的 HTTP 結構重新拿來用。
昨天是在問:
「一次 API 溝通裡有哪些東西?」
今天讀 API 文件時,則是在問:
「這一支 API 對這些東西到底怎麼規定?」
只要抓住這個順序,看到一大份 API 文件時,就比較知道該從哪裡開始。
但每個人都自己寫文件,格式不就又不一樣了嗎?
所以如果希望這份文件有一套更標準化的描述方式,而且工具也能解析,就會進到接下來的:
OpenAPI。
OpenAPI Specification(OpenAPI 規格) 是一套用來描述 HTTP API 的標準規格。
它提供一套結構化方式,去表達:
有哪些 Path?
有哪些 Method?
Parameters 怎麼定義?
Request Body 長什麼樣?
Response 長什麼樣?
需要什麼 Authentication?
所以 OpenAPI 不是另一份新的 API Contract。
比較接近:
API Contract
→ 規則本身
API Documentation
→ 把規則整理成可以查閱的文件
OpenAPI
→ 用標準化、結構化格式描述 HTTP API
前面那份「取得單一活動」的人類好讀版,如果換成一小段 OpenAPI,大概會看到這樣:
paths:
/api/activities/{id}:
get:
parameters:
- name: id
in: path
required: true
schema:
type: string
responses:
"200":
description: 取得活動成功
"404":
description: 找不到活動
現在不用學 YAML 語法。
只要把它跟前面那份 Contract 對照:
/api/activities/{id}
→ Path(路徑)
get
→ Method(HTTP 方法)
parameters
→ 這支 API 需要哪些參數
id
→ Path Parameter(路徑參數)
required: true
→ 必填
schema
→ 資料結構規格
responses
→ 可能出現的 Response(回應)
其實描述的還是同一件事。
差別只在於:
前面是方便人理解的整理方式;OpenAPI 則提供一套有固定結構、工具也能解析的描述方式。
BuJo 原本就有手寫的 API_DOCS.md,後來再把 API 文件整理成 OpenAPI,並搭配 swagger-jsdoc、swagger-ui-express,讓 API 可以透過 /api-docs 查看。
整理的過程中,我們也重新把文件和實際的 Route、Controller 一支一支對照。
這時才發現,有些地方兩邊其實沒有完全一致。
例如某支 API 在活動不存在時,文件原本記錄的是:
400
但 Code 實際回的是:
404
這件事反而讓我更理解 API 文件的價值。
文件不是寫完就永遠正確的答案,而是一份可以讓團隊共同查看、比對和維護的 Contract 描述。
API 改了,文件也要跟著更新;真的出現落差時,至少還有一個共同基準,可以把「原本約定的是什麼」和「現在實作的是什麼」攤開來確認。
我現在再看 API Contract,最有感的是:
只要使用端開始依賴某個 API 行為,它就不再只是 Backend 裡的一個實作細節,而是一份對外的承諾。
把這些規則寫清楚、留下來,真正重要的不是多一份文件,而是讓 Frontend、Backend,甚至之後接手的人,都知道現在共同遵守的是哪一套。
而這份「共同遵守的規則」,當然也不只管事情順利完成的時候。
原來連失敗之後要怎麼走,都要先排練!?
明天,就繼續來拆這條失敗路線。